#!/usr/bin/env python
# -*- coding: utf-8 -*-

"Control the monochromator CM-110"

import logging
import inspect
from serial import Serial

def show_frame():
    "Show info about the program place where called"
    stack = inspect.stack()
    for record in stack[1:]:
        print "Module %s, file %s, line %s, function %s." % (
                inspect.getmodule(record[0]), record[1], record[2], record[3])
        if record[5]:
            print "%d:" % record[5]
        for line in record[4]:
            print line,

PORT = "/dev/cuaU0"
LOG = logging.INFO # WARN INFO DEBUG
CONFIGDIR = "~/.flash-photolysis"
SAVEFILE = CONFIGDIR + "/NOVRAM"
REPRES = {
    ord("\r"): r"\r", #CR
    ord("\n"): r"\n", #LF
    ord("\t"): r"\t", #TAB
    ord("\0"): r"\0", #NUL
    27: r"\e", #ESC
    }

def repr_(byte):
    "Representing a byte for text"
    byte = ord(byte)
    if byte in REPRES:
        return REPRES[byte]
    if not (32 <= byte < 127):
        return "\\0%(byte)o" % ({"byte": byte})
    return chr(byte)

class CM110_Logs(object): # pylint: disable=C0103
    "Logging for Monochromator CM110/CM112 from CVI Laser Corp."

    def __init__(self, stream=None, port=PORT):
        self.port = Serial(port)
        self.port.setTimeout(30) # some operations need long time...
        self.logger = self.set_log(stream)

    def set_log(self, stream): # pylint: disable=R0201
        "Change the logger"
        logger = logging.getLogger('simple')
        logger.addHandler(logging.StreamHandler(stream))
        logger.setLevel(LOG)
        return logger

    def log_send(self, byte):
        "Write to a journal what will be sent, in few formats"
        self.logger.debug("Sending: int:%(byte)d hex:%(byte)x "
                          "oct:%(byte)o str:%(repr)s",
             {"byte" : ord(byte), "repr": repr_(byte)})

    def log_received(self, byte):
        "Write to a journal what was received, in few formats"
        self.logger.debug("Received: int:%(byte)d hex:%(byte)x "
                          "oct:%(byte)o str:%(repr)s"
            % {"byte" : ord(byte), "repr": repr_(byte)})

    def log_status(self, byte):
        "Write to a journal what is received status"
        byte = ord(byte)
        units = byte & 7
        if units == 0:
            units = "Microns (10nm)"
        elif units == 1:
            units = "nm"
        elif units == 2:
            units = "Å"
        else:
            units = " Wrong units!"
        result = "Status: " + " ".join((
            "Not accepted" if (byte & 128) else "Accepted",
            "No action" if (byte & 64) else "Action",
            "Too small" if (byte & 32) else "Too large",
            "" if (byte & 128) else "(irrelevant)",
            "Negative going" if (byte & 16) else "Positive going",
            "" if (byte & 128) else "(irrelevant)",
            "Negative orders" if (byte & 8) else "Positive orders",
            units,
            "(For SCAN:",
            "Lambda1 not acceptable" if (byte & 64) else "Lambda 1 OK",
            "Lambda2 not acceptable" if (byte & 32) else "Lambda 2 OK",
            ")",
        ))
        self.logger.info(result)

    def log_state(self, state):
        "Write to a journal the state of monochromator"
        self.logger.info("State of the monochromator CM110/CM112:")
        self.logger.info("\tWavelength: %d", state["position"])
        self.logger.info("\tType byte: %d", state["type"])
        self.logger.info("\tGrooves/mm: %d", state["grooves"])
        self.logger.info("\tBlaze, nm: %d", state["blaze"])
        self.logger.info("\tCurrent grating: %d", state["grating"])
        self.logger.info("\tSpeed, Å/s: %d", state["speed"])
        self.logger.info("\tSize: %d", state["size"])
        self.logger.info("\tNumber of gratings: %d", state["num_of_gratings"])
        self.logger.info("\tUnits (0=10nm 1=nm 2=Å): %d", state["units"])
        self.logger.info("\tSerial number: %d", state["serial_num"])

    def send(self, byte):
        "Log and send a byte"
        self.log_send(byte)
        self.port.write(byte)  # send to monochromator
        self.logger.debug("Send OK: %s" % repr_(byte))

    def receive(self, byte):
        "Receive, log, and check a byte. byte=None means any value is OK."
        resp = self.port.read(1) # read 1 byte from monochromator
        if resp == "":
            self.logger.critical("Timeout!")
            exit(1)
        self.log_received(resp)
        if byte is not None and resp != byte:
            self.logger.warn("ERROR: wrong reply, expected: %s, got: %s." % (
                            repr_(byte), repr_(resp)))
        else:
            self.logger.debug("Receive OK: %s" % repr_(resp))
        return resp

class CM110_Commands(CM110_Logs): # pylint: disable=C0103
    "Command for Monochromator CM110/CM112 from CVI Laser Corp."

    subq_table = {
        "position": 0,
        "type": 1,
        "grooves": 2,
        "blaze": 3,
        "grating": 4,
        "speed": 5,
        "size": 6,
        "num_of_gratings": 13,
        "units": 14,
        "serial_num": 19,
       }

    state_limits = {
        "position": {"type": "special"},
        "type": {"type": "list", "list": (0, 1, 254)},
        "grooves": {"type": "list", "list": (3600, 2400, 1800, 1200,
                                             600, 300, 150, 75)},
        "blaze": {"type": "limits", "limits": (0, 24000)},
        "grating": {"type": "list", "list": (1, 2)}, #<=num_of_gratings
        "speed": {"type": "special"},
        "size": {"type": "special"},
        "num_of_gratings": {"type": "list", "list": (1, 2)},
        "units": {"type": "list", "list": (0, 1, 2)}, # centimicron, nm, Å
        "serial_num": {"type": "None"},
       }

    def state_is_sane(self, state):
        "Check that some of the state values are legal"
        for subq, limits in self.state_limits.iteritems():
            if limits["type"] in ("None", "special"):
                continue
            elif ((limits["type"] == "list" and 
                   state[subq] not in limits["list"]) or
                 (limits["type"] == "limits" and not
                  (limits["limits"][0] <= state[subq] <= limits["limits"][1]))):
                self.logger.warn("state[%s] = %s not in %s!" %
                                    (subq, state[subq], limits["list"]))
                return False
        if not (0 < state["grating"] <= state["num_of_gratings"]):
            self.logger.warn("Selected grating %s not in %s!" %
                             (state["grating"], [0, state["num_of_gratings"]]))
            return False
        return True

    def echo(self):
        "Verify communication"
        self.send(chr(27))
        result = self.receive(chr(27))
        return result == chr(27)

    def goto(self, wavelength):
        "Set wavelength"
        self.send(chr(16))
        self.send(chr(wavelength>>8))  # high byte
        self.send(chr(wavelength%256)) # low byte
        status = self.receive(None)
        self.log_status(status)
        self.receive(chr(24))
        return status

    def query(self, subq):
        "Read current state of monochromater"
        assert (subq in self.subq_table or subq in self.subq_table.values()), \
                "Wrong subquery!"
        if subq in self.subq_table:
            subq = self.subq_table[subq]
        self.send(chr(56))
        self.send(chr(subq))
        high = ord(self.receive(None))
        low = ord(self.receive(None))
        status = self.receive(None)
        self.log_status(status)
        self.receive(chr(24))
        return high * 256 + low

    def query_all(self):
        "Read full current state of monochromater, return it as dict"
        result = {}
        for subq in self.subq_table.keys():
            result[subq] = self.query(subq)
        self.log_state(result)
        self.state_is_sane(result)
        return result

    def select(self, grating):
        "Select grating 1 or 2"
        if grating in ("1", "2"):
            grating = int(grating)
        assert grating in (1, 2), "Wrong grating number: %s." % grating
        self.send(chr(26))
        self.send(chr(grating))
        status = self.receive(None)
        self.log_status(status)
        self.receive(chr(24))

    def units(self, unit):
        "Select units for commands goto, scan, size, calibrate"
        unit_table = {"10nm": 0, "nm": 1, "Å": 2}
        assert (unit in unit_table or unit in unit_table.values()), \
                "Wrong units!"
        if unit in unit_table:
            unit = unit_table[unit]
        self.send(chr(50))
        self.send(chr(unit))
        status = self.receive(None)
        self.log_status(status)
        self.receive(chr(24))

    def readNOVRAM(self, address): # pylint: disable=C0103
        "Read a byte from NOVRAM in monochromator"
        assert (0 <= address <= 127), "Wrong NOVRAM address!"
        self.send(chr(156))
        self.send(chr(address))
        result = self.receive(None)
        status = self.receive(None)
        self.log_status(status)
        self.receive(chr(24))
        return result

    def readNOVRAM_all(self): # pylint: disable=C0103
        "Read all 128 bytes from monochromator NOVRAM"
        result = ""
        for addr in xrange(128):
            result += self.readNOVRAM(addr)
        return result

    def save_NOVRAM(self, fname=SAVEFILE): # pylint: disable=C0103
        "Save the contents of memory in a binary file"
        novram = self.readNOVRAM_all()
        with open(fname, "wb") as fil:
            fil.write(novram)

class CM110(CM110_Commands):
    "Class with the state of a CM100 monochromator"

    def __init__(self, stream=None, port=PORT):
        CM110_Commands.__init__(self, stream, port)
        self.state = self.query_all()

    def get_state(self):
        "Re-read the device state"
        self.state = self.query_all()

if __name__ == '__main__':
    CM = CM110_Commands()
    CM.logger.info("sendBreak")
    CM.port.sendBreak()
    CM.logger.info("flushInput")
    CM.port.flushInput()
    CM.logger.info("echo")
    CM.echo()
    CM.logger.info("echo")
    CM.echo()
    CM.logger.info("echo")
    CM.echo()
    CM.logger.info("select 2")
    CM.select(2)
    CM.logger.info("select 1")
    CM.select(1)
    CM.logger.info("units 0")
    CM.units(0)
    CM.logger.info("units nm")
    CM.units("nm")
    CM.logger.info("units 2")
    CM.units(2)
    CM.logger.info("goto 800")
    CM.goto(800)
    CM.logger.info("goto 0")
    CM.goto(0)
    CM.logger.info("query_all")
    CM.query_all()
    CM.logger.info("select 2")
    CM.select(2)
    CM.logger.info("goto 800")
    CM.goto(800)
    CM.logger.info("query_all")
    CM.query_all()
    CM.logger.info("readNOVRAM_all")
    CM.save_NOVRAM("NOVRAM")
